package fregat.base.util;

import sun.misc.Queue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.ResourceBundle;

/**
 * Logging class that allows multiple logging instances (logging categories).
 * Each instance may log to a file and/or to console. There are four logging
 * levels with ERROR as the lowest and DEBUG as the highest. When set to ERROR
 * only error messages logged with the <code>error(...)</code> method are
 * enabled, when set to DEBUG all logging calls are enabled.
 * Level NOTE should be used for developer notes that are only needed in
 * development.
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0
 */
public class Logger {
  public static int ERROR = 0; // critical failures
  public static int WARN = 1; // non-critical failures
  public static int INFO = 2; // user informations
  public static int DEBUG = 3; // debugging output
  public static int NOTE = 4; // developer notes

  public static String DEFAULT_CATEGORY = "default";
  public static final String[] levels = {"ERROR", "Warning", "Info", "Debug", "Note"};

  private static Hashtable categories; // stores logging instances
  private static Hashtable fileStreams; // stores file streams
  private int categoryLevel = INFO; // level of this logger
  private Hashtable outStreams; // out streams of this logger
  private String category; // category name of this logger
  private String fileKey; // key for file stream of this logger
  private boolean hideLevel; // flag, whether level names should be hidden in output
  private boolean hideCategory; // flag, whether category names should be hidden in output
  private boolean hideTimestamp; // flag, whether timestamp should be hidden in output
  private boolean logStackTrace; // flag, whether stack trace should be logged

  private Queue logQueue;
  private LogThread logThread;
  private boolean shutdown;


  private static ResourceBundle resource =
    ResourceBundle.getBundle("fregat.base.util.resource");

  private static final String SEPARATOR = // line separator
    (String) java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("line.separator"));

  private static final DateFormat TIMESTAMP =
    new SimpleDateFormat("dd.MM.yy HH:mm:ss.SSS");

  /**
   * Private constructor to avoid direct instancing. Use
   * <code>getCategory(...)</code> to retrieve a logger.
   *
   * @param category Logging category name
   */
  private Logger(String category) {
    this.category = category;
    categories.put(category, this);
    outStreams = new Hashtable();
    addOut(System.out, categoryLevel);
    logToConsole(false);
    /*
    // add shutdown hook
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        shutdown();
      }
    });
    */
  }

  public void shutdown() {
    shutdown = true;
    if (logThread != null) {
      logThread.shutdown();
      try {
        logThread.join(5000);
      } catch (InterruptedException ex) {
        ex.printStackTrace();
      }
      logThread = null;
    }
  }

  public static void shutdownAll() {
    Enumeration e = categories.elements();
    while (e.hasMoreElements()) {
      Logger category = (Logger) e.nextElement();
      category.shutdown();
    }
  }

  /**
   * Creates a new logging category and stores the instance in an internal table.
   * Use <code>getCategory(...)</code> to retrieve the logger.
   * Logger is created as console logger with level INFO.
   *
   * @param name Logging category name
   * @return Logger for specified category
   */
  public static Logger addCategory(String name) {
    if (categories == null)
      categories = new Hashtable();

    Logger category = (Logger) categories.get(name);

    if (category == null) {
      category = new Logger(name);
    }

    return category;
  }

  /**
   * @return Enumeration of all existing categories
   */
  public static Enumeration categories() {
    return categories.elements();
  }

  /**
   * @return Default logger
   */
  public static Logger getDefaultCategory() {
    return addCategory(DEFAULT_CATEGORY);
  }

  /**
   * Returns the logger for the given category name. If category does not exist
   * it is created then.
   *
   * @param name Logging category name
   * @return Logger for specified category
   */
  public static Logger getCategory(String name) {
    return addCategory(name);
  }

  /**
   * @return Enumeration of all existing loggers
   */
  public static Enumeration getCategories() {
    return categories.elements();
  }

  /**
   * @param category Name of logging category
   * @return True, if a logger for the category exists
   */
  public static boolean exists(String category) {
    return categories.get(category) != null;
  }

  /**
   * Sets whether logging should be written to console as well as to out stream.
   *
   * @param b True to log to console
   */
  public void logToConsole(boolean b) {
    ((StreamInfo) outStreams.get(System.out)).enabled = b;
  }

  /**
   * Set output stream for logging.
   *
   * @param out Stream to log on
   */
  public void setOut(PrintStream out) {
    addOut(out, categoryLevel);
  }

  /**
   * Adds an output stream for logging.
   *
   * @param out   Stream to log on
   * @param level Level for that stream
   */
  public void addOut(PrintStream out, int level) {
    StreamInfo info;
    if (!outStreams.containsKey(out)) {
      info = new StreamInfo();
      info.level = level;
      outStreams.put(out, info);
    } else {
      info = (StreamInfo) outStreams.get(out);
    }
    info.level = level;
    info.enabled = true;
  }

  /**
   * Removes the specified output stream from logging.
   *
   * @param out Stream to log on
   */
  public void removeOut(PrintStream out) {
    outStreams.remove(out);
  }

  /**
   * Only messages that contain one of the specified strings are sent to the
   * specified output stream .
   *
   * @param out    Stream to log on
   * @param filter Array of strings
   */
  public void setFilter(PrintStream out, String[] filter) {
    StreamInfo info = (StreamInfo) outStreams.get(out);
    if (info != null) {
      info.filter = filter;
    }
  }

  /**
   * Sets the logging level for the output stream.
   *
   * @param level Use ERROR,WARN,INFO,DEBUG
   */
  public void setLevel(int level) {
    categoryLevel = level;
    ((StreamInfo) outStreams.get(System.out)).level = level;

    if (fileKey != null) {
      getFileStream(fileKey).level = level;
    }
  }

  /**
   * @return Current logging level
   */
  public int getLevel() {
    return categoryLevel;
  }

  /**
   * (De-)activates display of category level, name, timestamp
   * in logging output.
   *
   * @param level     True, if not displayed
   * @param category  True, if not displayed
   * @param timestamp True, if not displayed
   */
  public void hide(boolean level, boolean category, boolean timestamp) {
    hideLevel = level;
    hideCategory = category;
    hideTimestamp = timestamp;
  }

  /**
   * Specifies whether stack trace should be logged or only be written to standard error.
   *
   * @param b True, to log stack trace
   */
  public void logStackTrace(boolean b) {
    logStackTrace = b;
  }

  /**
   * Sets the logging level for the console.
   *
   * @param level Use ERROR,WARN,INFO,DEBUG
   */
  public void setConsoleLevel(int level) {
    ((StreamInfo) outStreams.get(System.out)).level = level;
  }

  /**
   * @return Current console logging level
   */
  public int getConsoleLevel() {
    return ((StreamInfo) outStreams.get(System.out)).level;
  }


  /**
   * Adds timestamp to given string and writes to output if logging level is
   * set to warn or above.
   *
   * @param str Message string
   */
  public void warn(String str) {
    log(str, WARN);
  }

  /**
   * Adds timestamp to given string and writes to output if logging level is
   * set to debug or above.
   *
   * @param str Message string
   */
  public void debug(String str) {
    log(str, DEBUG);
  }

  /**
   * Adds timestamp to given string and exception and writes to output if
   * logging level is set to debug or above.
   *
   * @param str Message string
   * @param ex  Exception
   */
  public void debug(String str, Exception ex) {
    logException(str, ex, DEBUG);
  }

  /**
   * Adds timestamp to given string and writes to output if logging level is
   * set to info or above.
   *
   * @param str Message string
   */
  public void info(String str) {
    log(str, INFO);
  }

  /**
   * Adds timestamp to given string and writes to output if logging level is
   * set to error or above.
   *
   * @param str Message string
   */
  public void error(String str) {
    log(str, ERROR);
  }

  /**
   * Adds timestamp to given string and exception and writes to output if
   * logging level is set to error or above.
   *
   * @param str Message string
   * @param ex  Exception
   */
  public void error(String str, Exception ex) {
    logException(str, ex, ERROR);
  }

  /**
   * Adds timestamp to given string and writes to output if logging level is
   * set to note or above.
   * <br><i>Note: This level is for developer notes</i>
   *
   * @param str Message string
   */
  public void note(String str) {
    log(str, NOTE);
  }

  /**
   * Returns current date and time as timestamp.
   *
   * @return Formatted timestamp
   */
  public static String getTimestamp() {
    return formatTimestamp(System.currentTimeMillis());
  }

  /**
   * Returns the given time as timestamp
   *
   * @param lngTime Timestamp
   * @return Formatted timestamp
   */
  public static String formatTimestamp(long lngTime) {
    Date date = new Date(lngTime);
    return "[" + TIMESTAMP.format(date) + "]";

  }

  /**
   * Logs an exception.
   *
   * @param str   Prefix text
   * @param ex    Exception
   * @param level Logging level
   */
  private void logException(String str, Exception ex, int level) {
    String message = " --> [" + ex;

    // build exception information
    StackTraceElement elements[] = ex.getStackTrace();
    if (elements.length > 0) {
      message += "@" +
        elements[0].getClassName() + "." +
        elements[0].getMethodName() + "(" +
        elements[0].getFileName() + ":" +
        elements[0].getLineNumber() + ")] ";
    }

    log(str + message + resource.getString("Logger_logException_1"), level);

    if (logStackTrace) {
      // log stack strace
      for (int i = 0; i < elements.length; i++) {
        log(elements[i].toString(), level);
      }
    } else {
      // log stack trace to standard error
      System.err.println(getTimestamp());
      ex.printStackTrace();
    }
  }

  /**
   * Central method to initiate the output thread for the message.
   *
   * @param str   Message to be logged
   * @param level Logging Level
   */
  public void log(String str, int level) {
    if (!shutdown) {
      if (logThread == null) {
        logQueue = new Queue();
        logThread = new LogThread(category);
        logThread.start();
      }

      logQueue.enqueue(new LogEntry(str, level));

      synchronized (logQueue) {
        logQueue.notify();
      }
    } else {
      doLog(str, level);
    }
  }

  /**
   * Logs the given string if logging level is smaller or equal to currently
   * set level.
   *
   * @param str   String
   * @param level Logging level
   * @return Output string
   */
  private synchronized String doLog(String str, int level) {
    if (level >= 0 && !hideLevel) {
      str = levels[level] + ": " + str;
    }
    if (!category.equalsIgnoreCase(DEFAULT_CATEGORY) && !hideCategory) {
      str = category + " " + str;
    }
    if (!hideTimestamp) {
      str = getTimestamp() + " " + str;
    }

    // log to output stream(s)
    if (level == ERROR) {
      System.err.println(str);
    }
    Enumeration outStreamKeys = outStreams.keys();
    while (outStreamKeys.hasMoreElements()) {
      PrintStream out = (PrintStream) outStreamKeys.nextElement();
      StreamInfo info = (StreamInfo) outStreams.get(out);
      if (info.enabled && level <= info.level) {
        // check string for filter
        boolean bLog = false;
        if (info.filter != null) {
          int i = 0;
          while (i < info.filter.length && !bLog) {
            bLog = str.indexOf(info.filter[i]) > 0;
            i++;
          }
        } else {
          bLog = true;
        }
        if (bLog) {
          out.print(str + SEPARATOR);
          if (out != System.out) {
            out.print("\r");
          }
        }
      }
    }

    // log to file
    if (fileKey != null) {
      FileStream fileStream = (FileStream) fileStreams.get(fileKey);
      fileStream.write(str + SEPARATOR);
    }

    return str;
  }

  /**
   * Enables the logger to log a file.
   * <p/>
   * Logger logs to file until maximum size is reached, then file is moved to
   * next index. Latest log is in file without index.
   *
   * @param filename     Filename (if no extension provided '.log' is appended)
   * @param maxFilesize  Maximum size of a file
   * @param maxFilecount Maximum number of files
   */
  public void logToFile(String filename, long maxFilesize, int maxFilecount) {
    fileKey = filename;
    FileStream fileStream = getFileStream(fileKey);
    fileStream.maxFilecount = maxFilecount;
    fileStream.maxFilesize = maxFilesize;
  }

  /**
   * Enables the logger to log a file.
   * <p/>
   * Logger logs to file until maximum size is reached, then file is moved to
   * next index. Latest log is in file without index.
   * This method uses an existing log file of another category or creates a new
   * one with default size(4 MB) and count(4 files).
   *
   * @param filename Filename (if no extension provided '.log' is appended)
   */
  public void logToFile(String filename) {
    if (getFileStream(filename) != null) {
      fileKey = filename;
    } else {
      logToFile(filename, 4000000, 4);
    }
  }

  /**
   * @param pfilename Name of log file
   * @return Wrapper object for file stream
   */
  private FileStream getFileStream(String pfilename) {
    if (fileStreams == null) fileStreams = new Hashtable();
    FileStream fileStream = (FileStream) fileStreams.get(pfilename);

    if (fileStream == null) {
      fileStream = new FileStream(pfilename);
      fileStreams.put(pfilename, fileStream);
    }

    return fileStream;
  }

  /**
   * Performs the logging output in a thread.
   */
  private class LogThread extends Thread {
    private boolean shutdown;

    public LogThread(String name) {
      setName("LogThread '" + name + "'");
    }

    public void run() {
      while (!shutdown) {
        if (!logQueue.isEmpty()) {
          dequeue();
        } else {
          synchronized (logQueue) {
            try {
              logQueue.wait(1000);
            } catch (InterruptedException ex) {
              System.err.println(getTimestamp());
              ex.printStackTrace();
            }
          }
        }
      }

      while (!logQueue.isEmpty()) {
        dequeue();
      }
    }

    private void dequeue() {
      try {
        LogEntry entry = (LogEntry) logQueue.dequeue();
        doLog(entry.message, entry.level);
      } catch (InterruptedException ex) {
        System.err.println(getTimestamp());
        ex.printStackTrace();
      }
    }

    private void shutdown() {
      shutdown = true;

      synchronized (this) {
        notify();
      }
    }
  }

  private class LogEntry {
    int level;
    String message;

    public LogEntry(String message, int level) {
      this.level = level;
      this.message = message;
    }
  }

  /**
   * Additional attributes for an output stream. Stored in table <code>outStreams</code>
   * with corresponding <code>PrintStream</code> used as key.
   */
  private class StreamInfo {
    int level;
    boolean enabled;
    String[] filter;

    public StreamInfo() {
      level = categoryLevel;
      enabled = true;
    }
  }

  /**
   * Additional attributes for a file stream. Stored in table <code>fileStreams</code>
   * with filename as key.
   */
  private class FileStream {
    private int level = categoryLevel;
    private long maxFilesize;
    private int maxFilecount;
    private String filename;
    private String extension = ".log";
    private PrintStream out;

    public FileStream(String filename) {
      int position = filename.lastIndexOf(".");
      if (position > 0) {
        // use configured extension
        extension = filename.substring(position, filename.length());
        this.filename = filename.substring(0, position);
      } else {
        // use default extension '.log'
        this.filename = filename;
      }
    }

    /**
     * @param index Index of file
     * @return Log file name with index and extension
     */
    private String createLogfile(int index) {
      return filename + index + extension;
    }

    /**
     * Moves the files by shifting index.
     * (e.g. arc2.log -> arc3.log, arc1.log -> arc2.log, arc.log -> arc1.log
     * -> arc.log is created)
     */
    private synchronized void moveFiles() {
      if (out != null) out.close();

      for (int i = maxFilecount; i > 0; i--) {
        File f = new File(createLogfile(i));
        if (i == maxFilecount)
          f.delete();
        else
          f.renameTo(new File(createLogfile(i + 1)));
      }

      try {
        out = new PrintStream(new FileOutputStream(createLogfile(1), true));
      } catch (Exception ex) {
        error(resource.getString("Logger_moveFiles_1"), ex);
      }
    }

    /**
     * Checks file size and moves files if necessary.
     */
    public synchronized void checkFileSize() {
      File f = new File(createLogfile(1));
      if (f.length() > maxFilesize) {
        moveFiles();
      }
    }

    public void write(String str) {
      if (out == null) moveFiles();
      out.print(str);
      checkFileSize();
    }
  }
}

